﻿using System;
using System.IO;
#if !SILVERLIGHT
using System.Windows;
#endif
using System.Windows.Media.Imaging;

namespace FreshMeat.Xaml
{
   public static class PngEncoder
   {
      const int Adler32Base = 65521;
      const int Maxblock = 0xFFFF;
      static readonly byte[] header = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
      static readonly byte[] ihdr = {(byte)'I', (byte)'H', (byte)'D', (byte)'R'};
      static readonly byte[] gama = {(byte)'g', (byte)'A', (byte)'M', (byte)'A'};
      static readonly byte[] idat = {(byte)'I', (byte)'D', (byte)'A', (byte)'T'};
      static readonly byte[] iend = {(byte)'I', (byte)'E', (byte)'N', (byte)'D'};
      static readonly byte[] fourByteData = {0, 0, 0, 0};
      static readonly byte[] argb = {0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0};

      public static EncodableImage ToEncodable(BitmapSource source)
      {
         return new EncodableImage(source);
      }

      public static MemoryStream Encode(EncodableImage image)
      {
         var stream = Encode(image.Buffer, image.Width, image.Height);
         return stream;
      }

      static MemoryStream Encode(byte[] data, int width, int height)
      {
         var ms = new MemoryStream();

         // Write PNG header
         ms.Write(header, 0, header.Length);

         // Write IHDR
         //  Width:              4 bytes
         //  Height:             4 bytes
         //  Bit depth:          1 byte
         //  Color type:         1 byte
         //  Compression method: 1 byte
         //  Filter method:      1 byte
         //  Interlace method:   1 byte

         var size = BitConverter.GetBytes(width);
         argb[0] = size[3];
         argb[1] = size[2];
         argb[2] = size[1];
         argb[3] = size[0];

         size = BitConverter.GetBytes(height);
         argb[4] = size[3];
         argb[5] = size[2];
         argb[6] = size[1];
         argb[7] = size[0];

         // Write IHDR chunk
         WriteChunk(ms, ihdr, argb);

         // Set gamma = 1
         size = BitConverter.GetBytes(1 * 100000);
         fourByteData[0] = size[3];
         fourByteData[1] = size[2];
         fourByteData[2] = size[1];
         fourByteData[3] = size[0];

         // Write gAMA chunk
         WriteChunk(ms, gama, fourByteData);

         // Write IDAT chunk
         uint widthLength = (uint)(width * 4) + 1;
         uint dcSize = widthLength * (uint)height;

         // First part of ZLIB header is 78 1101 1010 (DA) 0000 00001 (01)
         // ZLIB info
         //
         // CMF Byte: 78
         //  CINFO = 7 (32K window size)
         //  CM = 8 = (deflate compression)
         // FLG Byte: DA
         //  FLEVEL = 3 (bits 6 and 7 - ignored but signifies max compression)
         //  FDICT = 0 (bit 5, 0 - no preset dictionary)
         //  FCHCK = 26 (bits 0-4 - ensure CMF*256+FLG / 31 has no remainder)
         // Compressed data
         //  FLAGS: 0 or 1
         //    00000 00 (no compression) X (X=1 for last block, 0=not the last block)
         //    LEN = length in bytes (equal to ((width*4)+1)*height
         //    NLEN = one's compliment of LEN
         //    Example: 1111 1011 1111 1111 (FB), 0000 0100 0000 0000 (40)
         //    Data for each line: 0 [RGBA] [RGBA] [RGBA] ...
         //    ADLER32

         var adler = ComputeAdler32(data);

         var comp = new MemoryStream();

         // Calculate number of 64K blocks
         uint rowsPerBlock = Maxblock / widthLength;
         uint blockSize = rowsPerBlock * widthLength;
         uint blockCount;
         uint remainder = dcSize;

         if ((dcSize % blockSize) == 0)
         {
            blockCount = dcSize / blockSize;
         }
         else
         {
            blockCount = (dcSize / blockSize) + 1;
         }

         // Write headers
         comp.WriteByte(0x78);
         comp.WriteByte(0xDA);

         for (uint blocks = 0; blocks < blockCount; blocks++)
         {
            // Write LEN
            var length = (ushort)((remainder < blockSize) ? remainder : blockSize);

            comp.WriteByte(length == remainder ? (byte)0x01 : (byte)0x00);

            comp.Write(BitConverter.GetBytes(length), 0, 2);

            // Write one's compliment of LEN
            comp.Write(BitConverter.GetBytes((ushort)~length), 0, 2);

            // Write blocks
            comp.Write(data, (int)(blocks * blockSize), length);

            // Next block
            remainder -= blockSize;
         }

         WriteReversedBuffer(comp, BitConverter.GetBytes(adler));
         comp.Seek(0, SeekOrigin.Begin);

         var dat = new byte[comp.Length];
         comp.Read(dat, 0, (int)comp.Length);

         WriteChunk(ms, idat, dat);

         // Write IEND chunk
         WriteChunk(ms, iend, new byte[0]);

         // Reset stream
         ms.Seek(0, SeekOrigin.Begin);

         return ms;

         // See http://www.libpng.org/pub/png//spec/1.2/PNG-Chunks.html
         // See http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.4
         // See http://www.gzip.org/zlib/rfc-zlib.html (ZLIB format)
         // See ftp://ftp.uu.net/pub/archiving/zip/doc/rfc1951.txt (ZLIB compression format)
      }

      static void WriteReversedBuffer(Stream stream, byte[] data)
      {
         int size = data.Length;
         var reorder = new byte[size];

         for (var idx = 0; idx < size; idx++)
         {
            reorder[idx] = data[size - idx - 1];
         }
         stream.Write(reorder, 0, size);
      }

      static void WriteChunk(Stream stream, byte[] type, byte[] data)
      {
         int idx;
         int size = type.Length;
         var buffer = new byte[type.Length + data.Length];

         // Initialize buffer
         for (idx = 0; idx < type.Length; idx++)
         {
            buffer[idx] = type[idx];
         }

         for (idx = 0; idx < data.Length; idx++)
         {
            buffer[idx + size] = data[idx];
         }

         // Write length
         WriteReversedBuffer(stream, BitConverter.GetBytes(data.Length));

         // Write type and data
         stream.Write(buffer, 0, buffer.Length); // Should always be 4 bytes

         // Compute and write the CRC
         WriteReversedBuffer(stream, BitConverter.GetBytes(GetCrc(buffer)));
      }

      static readonly uint[] crcTable = new uint[256];
      static bool crcTableComputed;

      static void MakeCrcTable()
      {
         for (int n = 0; n < 256; n++)
         {
            var c = (uint)n;
            for (int k = 0; k < 8; k++)
            {
               if ((c & (0x00000001)) > 0)
                  c = 0xEDB88320 ^ (c >> 1);
               else
                  c = c >> 1;
            }
            crcTable[n] = c;
         }

         crcTableComputed = true;
      }

      static uint UpdateCrc(uint crc, byte[] buf, int len)
      {
         uint c = crc;

         if (!crcTableComputed)
         {
            MakeCrcTable();
         }

         for (int n = 0; n < len; n++)
         {
            c = crcTable[(c ^ buf[n]) & 0xFF] ^ (c >> 8);
         }

         return c;
      }

      /* Return the CRC of the bytes buf[0..len-1]. */

      static uint GetCrc(byte[] buf)
      {
         return UpdateCrc(0xFFFFFFFF, buf, buf.Length) ^ 0xFFFFFFFF;
      }

      static uint ComputeAdler32(byte[] buf)
      {
         UInt32 a = 1;
         UInt32 b = 0;
         int length = buf.Length;

         int chunks = length / 5550;
         int remainder = length % 5550;
         int startIndex = 0;

         for (int i = 0; i < chunks; i++)
         {
            int endIndex = startIndex + 5550;
            for (int index = startIndex; index < endIndex; index++)
            {
               a += buf[index];
               b += a;
            }

            a %= Adler32Base;
            b %= Adler32Base;
            startIndex = endIndex;
         }

         int endIndex1 = startIndex + remainder;
         for (int index = startIndex; index < endIndex1; index++)
         {
            a += buf[index];
            b += a;
         }
         a %= Adler32Base;
         b %= Adler32Base;

         return (b << 16) + a;
      }

      public class EncodableImage
      {
         public readonly int Width;
         public readonly int Height;
         public readonly byte[] Buffer;
         readonly int rowLength;

         public EncodableImage(BitmapSource source)
         {
            var bitmap = source is WriteableBitmap ? (WriteableBitmap)source : new WriteableBitmap(source);

            Width = bitmap.PixelWidth;
            Height = bitmap.PixelHeight;

            if (Width <= 0 || Width > 2047)
               throw new ArgumentOutOfRangeException("source", "Width must be between 0 and 2047.");
            if (Height <= 0 || Height > 2047)
               throw new ArgumentOutOfRangeException("source", "Width must be between 0 and 2047.");

            rowLength = Width * 4 + 1;
            Buffer = new byte[rowLength * Height];

            for (var idx = 0; idx < Height; idx++)
               Buffer[idx * rowLength] = 0; // Filter bit

            for (var y = 0; y < bitmap.PixelHeight; ++y)
            {
#if !SILVERLIGHT
               var row = new byte[rowLength];
               bitmap.CopyPixels(new Int32Rect(0, y, Width, 1), row, rowLength, 0);
#endif
               for (var x = 0; x < bitmap.PixelWidth; ++x)
               {
#if SILVERLIGHT
                  int pixel = bitmap.Pixels[bitmap.PixelWidth * y + x];
                  SetPixel(x, y,
                           (byte)((pixel >> 16) & 0xFF),
                           (byte)((pixel >> 8) & 0xFF),
                           (byte)(pixel & 0xFF),
                           (byte)((pixel >> 24) & 0xFF));
#else
                  SetPixel(x, y, row[x * 4 + 2], row[x * 4 + 1], row[x * 4 + 0], row[x * 4 + 3]);
#endif
               }
            }
         }

         //public void SetPixel(int col, int row, Color color)
         //{
         //   SetPixel(col, row, color.R, color.G, color.B, color.A);
         //}

         void SetPixel(int col, int row, byte red, byte green, byte blue, byte alpha)
         {
            var start = rowLength * row + col * 4 + 1;
            Buffer[start] = red;
            Buffer[start + 1] = green;
            Buffer[start + 2] = blue;
            Buffer[start + 3] = alpha;
         }

         //public Color GetPixel(int col, int row)
         //{
         //   if (col > Width || col < 0)
         //      throw new ArgumentOutOfRangeException("col", "Column must be greater than 0 and less than the Width");
         //   if (row > Height || row < 0)
         //      throw new ArgumentOutOfRangeException("row", "Row must be greater than 0 and less than the Height");

         //   var color = new Color();
         //   var _base = rowLength * row + col + 1;

         //   color.R = Buffer[_base];
         //   color.G = Buffer[_base + 1];
         //   color.B = Buffer[_base + 2];
         //   color.A = Buffer[_base + 3];

         //   return color;
         //}
      }
   }
}
